{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "4ba51a29-a566-4b5a-97f0-10e634567e40",
   "metadata": {
    "collapsed": true,
    "jupyter": {
     "outputs_hidden": true
    }
   },
   "source": [
    "# Transform OpenAPI apis into MCP tools using Bedrock AgentCore Gateway\n",
    "\n",
    "## Overview\n",
    "Customers can bring OpenAPI spec in JSON or YAML and transform the apis into MCP tools using Bedrock AgentCore Gateway. We will demonstrate building a Mars Weather agent that calls NASA's Open APIs using API key. \n",
    "\n",
    "The Gateway workflow involves the following steps to connect your agents to external tools:\n",
    "* **Create the tools for your Gateway** - Define your tools using schemas such as OpenAPI specifications for REST APIs. The OpenAPI specifications are then parsed by Amazon Bedrock AgentCore for creating the Gateway.\n",
    "* **Create a Gateway endpoint** - Create the gateway that will serve as the MCP entry point with inbound authentication.\n",
    "* **Add targets to your Gateway** - Configure the OpenAPI targets that define how the gateway routes requests to specific tools. All the APIs that part of OpenAPI file will become an MCP-compatible tool, and will be made available through your Gateway endpoint URL. Configure outbound authorization for each OpenAPI Gateway target. \n",
    "* **Update your agent code** - Connect your agent to the Gateway endpoint to access all configured tools through the unified MCP interface.\n",
    "\n",
    "![How does it work](images/openapi-gateway-apikey.png)\n",
    "\n",
    "### Tutorial Details\n",
    "\n",
    "\n",
    "| Information          | Details                                                   |\n",
    "|:---------------------|:----------------------------------------------------------|\n",
    "| Tutorial type        | Interactive                                               |\n",
    "| AgentCore components | AgentCore Gateway, AgentCore Identity                     |\n",
    "| Agentic Framework    | Strands Agents                                            |\n",
    "| Gateway Target type  | OpenAPI                                                   |\n",
    "| Agent                | Mars Weather agent                                        |\n",
    "| Inbound Auth IdP     | Amazon Cognito                                            |\n",
    "| Outbound Auth        | API Key                                                   |\n",
    "| LLM model            | Anthropic Claude Sonnet 3.7, Amazon Nova Pro              |\n",
    "| Tutorial components  | Creating AgentCore Gateway and Invoking AgentCore Gateway |\n",
    "| Tutorial vertical    | Cross-vertical                                            |\n",
    "| Example complexity   | Easy                                                      |\n",
    "| SDK used             | boto3                                                     |\n",
    "\n",
    "In the first part of the tutorial we will create some AmazonCore Gateway targets\n",
    "\n",
    "### Tutorial Architecture\n",
    "In this tutorial we will transform operations defined in OpenAPI yaml/json file into MCP tools and host it in Bedrock AgentCore Gateway.\n",
    "For demonstration purposes, we will build a Mars Weather agent that answers queries related to weather in Mars. The agent uses Open APIs of NASA. The solution uses Strands Agent using Amazon Bedrock models\n",
    "In our example we will use a very simple agent with getInsightWeather tool for Mars weather."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5362e1ad-f027-4452-a8d9-0b861c0115c2",
   "metadata": {},
   "source": [
    "## Prerequisites\n",
    "\n",
    "To execute this tutorial you will need:\n",
    "* Jupyter notebook (Python kernel)\n",
    "* uv\n",
    "* AWS credentials\n",
    "* Amazon Cognito"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a17e1d70-9a76-42ce-b1ac-1c3a0bf4d12c",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Make sure you download the latest botocore and boto3 libraries.\n",
    "import shutil\n",
    "import subprocess\n",
    "import sys\n",
    "\n",
    "def ensure_uv_installed():\n",
    "    if shutil.which(\"uv\") is None:\n",
    "        print(\"🔧 'uv' not found. Installing with pip...\")\n",
    "        subprocess.check_call([sys.executable, \"-m\", \"pip\", \"install\", \"uv\"])\n",
    "    else:\n",
    "        print(\"✅ 'uv' is already installed.\")\n",
    "\n",
    "def uv_install(*packages):\n",
    "    ensure_uv_installed()\n",
    "    uv_path = shutil.which(\"uv\")\n",
    "    print(f\"📦 Installing {', '.join(packages)} using uv...\")\n",
    "    subprocess.check_call([uv_path, \"pip\", \"install\", *packages])\n",
    "\n",
    "uv_install(\"botocore\", \"boto3\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a4e6248d-b740-418b-ae0d-c0a9623e43e1",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Set AWS credentials if not using Amazon SageMaker notebook\n",
    "import os\n",
    "os.environ['AWS_ACCESS_KEY_ID'] = ''\n",
    "os.environ['AWS_SECRET_ACCESS_KEY'] = ''\n",
    "os.environ['AWS_DEFAULT_REGION'] = 'us-east-1'"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "4ca94412-7337-4293-ab74-aa2e77507fd0",
   "metadata": {},
   "outputs": [],
   "source": [
    "import os\n",
    "import sys\n",
    "\n",
    "# Get the directory of the current script\n",
    "if '__file__' in globals():\n",
    "    current_dir = os.path.dirname(os.path.abspath(__file__))\n",
    "else:\n",
    "    current_dir = os.getcwd()  # Fallback if __file__ is not defined (e.g., Jupyter)\n",
    "\n",
    "# Navigate to the directory containing utils.py (one level up)\n",
    "utils_dir = os.path.abspath(os.path.join(current_dir, '../..'))\n",
    "\n",
    "# Add to sys.path\n",
    "sys.path.insert(0, utils_dir)\n",
    "\n",
    "# Now you can import utils\n",
    "import utils"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "22f4cb2b-14b0-4171-982a-19a1ed4e8ad2",
   "metadata": {},
   "outputs": [],
   "source": [
    "#### Create an IAM role for the Gateway to assume\n",
    "import utils\n",
    "\n",
    "agentcore_gateway_iam_role = utils.create_agentcore_gateway_role(\"sample-lambdagateway\")\n",
    "print(\"Agentcore gateway role ARN: \", agentcore_gateway_iam_role['Role']['Arn'])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "65120594-c3ec-4d51-810b-8d478851d8d2",
   "metadata": {},
   "source": [
    "# Create Amazon Cognito Pool for Inbound authorization to Gateway"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "32c382f0-aefe-4756-8e06-fac3a7b96e1c",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Creating Cognito User Pool \n",
    "import os\n",
    "import boto3\n",
    "import requests\n",
    "import time\n",
    "from botocore.exceptions import ClientError\n",
    "\n",
    "REGION = os.environ['AWS_DEFAULT_REGION']\n",
    "USER_POOL_NAME = \"sample-agentcore-gateway-pool\"\n",
    "RESOURCE_SERVER_ID = \"sample-agentcore-gateway-id\"\n",
    "RESOURCE_SERVER_NAME = \"sample-agentcore-gateway-name\"\n",
    "CLIENT_NAME = \"sample-agentcore-gateway-client\"\n",
    "SCOPES = [\n",
    "    {\"ScopeName\": \"gateway:read\", \"ScopeDescription\": \"Read access\"},\n",
    "    {\"ScopeName\": \"gateway:write\", \"ScopeDescription\": \"Write access\"}\n",
    "]\n",
    "scopeString = f\"{RESOURCE_SERVER_ID}/gateway:read {RESOURCE_SERVER_ID}/gateway:write\"\n",
    "\n",
    "cognito = boto3.client(\"cognito-idp\", region_name=REGION)\n",
    "\n",
    "print(\"Creating or retrieving Cognito resources...\")\n",
    "user_pool_id = utils.get_or_create_user_pool(cognito, USER_POOL_NAME)\n",
    "print(f\"User Pool ID: {user_pool_id}\")\n",
    "\n",
    "utils.get_or_create_resource_server(cognito, user_pool_id, RESOURCE_SERVER_ID, RESOURCE_SERVER_NAME, SCOPES)\n",
    "print(\"Resource server ensured.\")\n",
    "\n",
    "client_id, client_secret  = utils.get_or_create_m2m_client(cognito, user_pool_id, CLIENT_NAME, RESOURCE_SERVER_ID)\n",
    "print(f\"Client ID: {client_id}\")\n",
    "\n",
    "# Get discovery URL  \n",
    "cognito_discovery_url = f'https://cognito-idp.{REGION}.amazonaws.com/{user_pool_id}/.well-known/openid-configuration'\n",
    "print(cognito_discovery_url)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f1a63450-7fb9-42fc-ab4f-3d86c27bb2f8",
   "metadata": {},
   "source": [
    "# Create the Gateway "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "72f2cd57-7777-42d3-b6f3-c45ed0a935c4",
   "metadata": {},
   "outputs": [],
   "source": [
    "# CreateGateway with Cognito authorizer without CMK. Use the Cognito user pool created in the previous step\n",
    "import boto3\n",
    "gateway_client = boto3.client('bedrock-agentcore-control', region_name = os.environ['AWS_DEFAULT_REGION'])\n",
    "auth_config = {\n",
    "    \"customJWTAuthorizer\": { \n",
    "        \"allowedClients\": [client_id],  # Client MUST match with the ClientId configured in Cognito. Example: 7rfbikfsm51j2fpaggacgng84g\n",
    "        \"discoveryUrl\": cognito_discovery_url\n",
    "    }\n",
    "}\n",
    "create_response = gateway_client.create_gateway(name='DemoGWOpenAPIAPIKeyNasaOAI',\n",
    "    roleArn = agentcore_gateway_iam_role['Role']['Arn'], # The IAM Role must have permissions to create/list/get/delete Gateway \n",
    "    protocolType='MCP',\n",
    "    authorizerType='CUSTOM_JWT',\n",
    "    authorizerConfiguration=auth_config, \n",
    "    description='AgentCore Gateway with OpenAPI target'\n",
    ")\n",
    "print(create_response)\n",
    "# Retrieve the GatewayID used for GatewayTarget creation\n",
    "gatewayID = create_response[\"gatewayId\"]\n",
    "gatewayURL = create_response[\"gatewayUrl\"]\n",
    "print(gatewayID)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1345c920-3921-40c7-9c00-110e8d02184b",
   "metadata": {},
   "source": [
    "# Transforming NASA Open APIs into MCP tools using Bedrock AgentCore Gateway"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4765ddea-a86d-492e-90f9-ffb14e1b3b74",
   "metadata": {},
   "source": [
    "We are going to have a Mars Weather agent getting weather data from Nasa's Open APIs. You will need to register for Nasa Insight API [here](https://api.nasa.gov/). It's free! Once you register, you will get an API Key in your email. Use the API key to configure the credentials provider for creating the OpenAPI target."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "64caa620-02ec-424a-a8b9-eb00b39289bf",
   "metadata": {},
   "outputs": [],
   "source": [
    "import boto3\n",
    "from pprint import pprint\n",
    "from botocore.config import Config\n",
    "\n",
    "acps = boto3.client(service_name=\"bedrock-agentcore-control\")\n",
    "\n",
    "response=acps.create_api_key_credential_provider(\n",
    "    name=\"NasaInsightAPIKey\", \n",
    "    apiKey=\"\", # Get an API key by signing up at api.nasa.gov. Takes 2-min to get an API key in your email.\n",
    ")\n",
    "\n",
    "pprint(response)\n",
    "credentialProviderARN = response['credentialProviderArn']\n",
    "pprint(f\"Egress Credentials provider ARN, {credentialProviderARN}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "510b092f-be1d-4b31-abb7-1fb8cab887ef",
   "metadata": {},
   "source": [
    "# Create an OpenAPI target "
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c1cb1835-5aaf-471d-ac28-6f5fe78caf5a",
   "metadata": {},
   "source": [
    "#### Upload the NASA Open API json file in S3"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a105be4e-ae05-4981-a609-01d410f66023",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Create an S3 client\n",
    "session = boto3.session.Session()\n",
    "s3_client = session.client('s3')\n",
    "sts_client = session.client('sts')\n",
    "\n",
    "# Retrieve AWS account ID and region\n",
    "account_id = sts_client.get_caller_identity()[\"Account\"]\n",
    "region = session.region_name\n",
    "# Define parameters\n",
    "bucket_name = '' # Your s3 bucket to upload the OpenAPI json file.\n",
    "file_path = 'openapi-specs/nasa_mars_insights_openapi.json'\n",
    "object_key = 'nasa_mars_insights_openapi.json'\n",
    "# Upload the file using put_object and read response\n",
    "try:\n",
    "    with open(file_path, 'rb') as file_data:\n",
    "        response = s3_client.put_object(Bucket=bucket_name, Key=object_key, Body=file_data)\n",
    "\n",
    "    # Construct the ARN of the uploaded object with account ID and region\n",
    "    openapi_s3_uri = f's3://{bucket_name}/{object_key}'\n",
    "    print(f'Uploaded object S3 URI: {openapi_s3_uri}')\n",
    "except Exception as e:\n",
    "    print(f'Error uploading file: {e}')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "715f371d-3118-4c7f-bb23-c90ab09e4284",
   "metadata": {},
   "source": [
    "#### Configure outbound auth and Create the gateway target"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "629945ac-d1c3-49ae-92f7-4b0e0ef6d2c8",
   "metadata": {},
   "outputs": [],
   "source": [
    "# S3 Uri for OpenAPI spec file\n",
    "nasa_openapi_s3_target_config = {\n",
    "    \"mcp\": {\n",
    "          \"openApiSchema\": {\n",
    "              \"s3\": {\n",
    "                  \"uri\": openapi_s3_uri\n",
    "              }\n",
    "          }\n",
    "      }\n",
    "}\n",
    "\n",
    "# API Key credentials provider configuration\n",
    "api_key_credential_config = [\n",
    "    {\n",
    "        \"credentialProviderType\" : \"API_KEY\", \n",
    "        \"credentialProvider\": {\n",
    "            \"apiKeyCredentialProvider\": {\n",
    "                    \"credentialParameterName\": \"api_key\", # Replace this with the name of the api key name expected by the respective API provider. For passing token in the header, use \"Authorization\"\n",
    "                    \"providerArn\": credentialProviderARN,\n",
    "                    \"credentialLocation\":\"QUERY_PARAMETER\", # Location of api key. Possible values are \"HEADER\" and \"QUERY_PARAMETER\".\n",
    "                    #\"credentialPrefix\": \" \" # Prefix for the token. Valid values are \"Basic\". Applies only for tokens.\n",
    "            }\n",
    "        }\n",
    "    }\n",
    "  ]\n",
    "\n",
    "targetname='DemoOpenAPITargetS3NasaMars'\n",
    "response = gateway_client.create_gateway_target(\n",
    "    gatewayIdentifier=gatewayID,\n",
    "    name=targetname,\n",
    "    description='OpenAPI Target with S3Uri using SDK',\n",
    "    targetConfiguration=nasa_openapi_s3_target_config,\n",
    "    credentialProviderConfigurations=api_key_credential_config)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d3ac6532-5299-4024-917d-bcd60caea6ed",
   "metadata": {},
   "source": [
    "# Calling Bedrock AgentCore Gateway from a Strands Agent\n",
    "\n",
    "The Strands agent seamlessly integrates with AWS tools through the Bedrock AgentCore Gateway, which implements the Model Context Protocol (MCP) specification. This integration enables secure, standardized communication between AI agents and AWS services.\n",
    "\n",
    "At its core, the Bedrock AgentCore Gateway serves as a protocol-compliant Gateway that exposes fundamental MCP APIs: ListTools and InvokeTools. These APIs allow any MCP-compliant client or SDK to discover and interact with available tools in a secure, standardized way. When the Strands agent needs to access AWS services, it communicates with the Gateway using these MCP-standardized endpoints.\n",
    "\n",
    "The Gateway's implementation adheres strictly to the (MCP Authorization specification)[https://modelcontextprotocol.org/specification/draft/basic/authorization], ensuring robust security and access control. This means that every tool invocation by the Strands agent goes through authorization step, maintaining security while enabling powerful functionality.\n",
    "\n",
    "For example, when the Strands agent needs to access MCP tools, it first calls ListTools to discover available tools, then uses InvokeTools to execute specific actions. The Gateway handles all the necessary security validations, protocol translations, and service interactions, making the entire process seamless and secure.\n",
    "\n",
    "This architectural approach means that any client or SDK that implements the MCP specification can interact with AWS services through the Gateway, making it a versatile and future-proof solution for AI agent integrations."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3e3a7c36-d910-4651-9f1e-e2b5f605440a",
   "metadata": {},
   "outputs": [],
   "source": [
    "uv_install(\"mcp[cli]\", \"strands-agents\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "873031fe-62b5-4196-91be-500c1f87dfd4",
   "metadata": {},
   "source": [
    "# Request the access token from Amazon Cognito for inbound authorization"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3ed1d1d6-e84c-4286-bf25-3ad6a49723b9",
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"Requesting the access token from Amazon Cognito authorizer...May fail for some time till the domain name propogation completes\")\n",
    "token_response = utils.get_token(user_pool_id, client_id, client_secret,scopeString,REGION)\n",
    "token = token_response[\"access_token\"]\n",
    "print(\"Token response:\", token)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6fd0379d-9576-43cb-aa9b-72b86c43b472",
   "metadata": {},
   "source": [
    "# Ask Mars weather agent by calling NASA Open APIs using Bedrock AgentCore Gateway"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "be4b39c4-7387-4ce8-b728-f2347fbdaa36",
   "metadata": {},
   "outputs": [],
   "source": [
    "from strands.models import BedrockModel\n",
    "from mcp.client.streamable_http import streamablehttp_client \n",
    "from strands.tools.mcp.mcp_client import MCPClient\n",
    "from strands import Agent\n",
    "\n",
    "def create_streamable_http_transport():\n",
    "    return streamablehttp_client(gatewayURL,headers={\"Authorization\": f\"Bearer {token}\"})\n",
    "\n",
    "client = MCPClient(create_streamable_http_transport)\n",
    "\n",
    "## The IAM group/user/ configured in ~/.aws/credentials should have access to Bedrock model\n",
    "yourmodel = BedrockModel(\n",
    "    model_id=\"us.amazon.nova-pro-v1:0\",\n",
    "    temperature=0.7,\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e64794e7-4e5f-4fc5-824a-61c901e356c4",
   "metadata": {},
   "outputs": [],
   "source": [
    "from strands import Agent\n",
    "import logging\n",
    "\n",
    "\n",
    "# Configure the root strands logger. Change it to DEBUG if you are debugging the issue.\n",
    "logging.getLogger(\"strands\").setLevel(logging.INFO)\n",
    "\n",
    "# Add a handler to see the logs\n",
    "logging.basicConfig(\n",
    "    format=\"%(levelname)s | %(name)s | %(message)s\", \n",
    "    handlers=[logging.StreamHandler()]\n",
    ")\n",
    "\n",
    "with client:\n",
    "    # Call the listTools \n",
    "    tools = client.list_tools_sync()\n",
    "    # Create an Agent with the model and tools\n",
    "    agent = Agent(model=yourmodel,tools=tools) ## you can replace with any model you like\n",
    "    print(f\"Tools loaded in the agent are {agent.tool_names}\")\n",
    "    print(f\"Tools configuration in the agent are {agent.tool_config}\")\n",
    "    # Invoke the agent with the sample prompt. This will only invoke  MCP listTools and retrieve the list of tools the LLM has access to. The below does not actually call any tool.\n",
    "    agent(\"Hi , can you list all tools available to you\")\n",
    "    agent(\"What is the weather in northern part of the mars\")\n",
    "    # Invoke the agent with sample prompt, invoke the tool and display the response\n",
    "    #Call the MCP tool explicitly. The MCP Tool name and arguments must match with your AWS Lambda function or the OpenAPI/Smithy API\n",
    "    result = client.call_tool_sync(\n",
    "    tool_use_id=\"get-insight-weather-1\", # You can replace this with unique identifier. \n",
    "    name=targetname+\"___getInsightWeather\", # This is the tool name based on AWS Lambda target types. This will change based on the target name\n",
    "    arguments={\"ver\": \"1.0\",\"feedtype\": \"json\"}\n",
    "    )\n",
    "    #Print the MCP Tool response\n",
    "    print(f\"Tool Call result: {result['content'][0]['text']}\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "96f9ab38-fddc-454d-81fe-d6aa5c958292",
   "metadata": {},
   "source": [
    "**Issue: if you get below error while executing below cell, it indicates incompatibily between pydantic and pydantic-core versions.**\n",
    "\n",
    "```\n",
    "TypeError: model_schema() got an unexpected keyword argument 'generic_origin'\n",
    "```\n",
    "**How to resolve?**\n",
    "\n",
    "You will need to make sure you have pydantic==2.7.2 and pydantic-core 2.27.2 that are both compatible. Restart the kernel once done."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fa8dd91d-15a6-45b2-b539-bb3eaf0e0e08",
   "metadata": {},
   "source": [
    "# Clean up\n",
    "Additional resources are also created like IAM role, IAM Policies, Credentials provider, AWS Lambda functions, Cognito user pools, s3 buckets that you might need to manually delete as part of the clean up. This depends on the example you run."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a22f158d-83b6-4d65-892f-9a208af3d742",
   "metadata": {},
   "source": [
    "## Delete the gateway (Optional)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "58d250fc-dd34-4eb3-8d5b-c9eac9de9c79",
   "metadata": {},
   "outputs": [],
   "source": [
    "import utils\n",
    "utils.delete_gateway(gateway_client,gatewayID)"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.9"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
